import multer from "multer"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { callNodeListener, getRequestIP } from "h3"; import { POST_MEDIA_PUBLIC_PREFIX } from "#server/constants/media"; import { replaceMediaAssetFileFromTempUpload } from "#server/service/media"; import { requireAdmin } from "#server/utils/admin-guard"; import { R } from "#server/utils/response"; import { assertUnderRateLimit } from "#server/utils/simple-rate-limit"; type MulterUploadedFile = { originalname: string; filename: string; path: string; mimetype: string; size: number; }; export default defineWrappedResponseHandler(async (event) => { const admin = await requireAdmin(event); const ip = getRequestIP(event, { xForwardedFor: true }) ?? "unknown"; assertUnderRateLimit(`admin-media-asset-reupload:${ip}`, 40, 60_000); const idRaw = getRouterParam(event, "id"); const assetId = Number(idRaw); if (!Number.isInteger(assetId) || assetId < 1) { throw createError({ statusCode: 400, statusMessage: "无效的资源 id" }); } const upload = multer({ storage: multer.diskStorage({ destination: (_req, _file, cb) => { cb(null, os.tmpdir()); }, filename: (_req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); const baseName = path.basename(file.originalname, ext).replace(/[^a-z0-9]/gi, "-"); cb(null, `reupload-${Date.now()}-${Math.round(Math.random() * 1e9)}-${baseName}${ext}`); }, }), limits: { fileSize: 10 * 1024 * 1024 }, fileFilter: (_req, file, cb) => { const allowed = ["image/png", "image/jpeg", "image/jpg", "image/webp"]; if (allowed.includes(file.mimetype)) { cb(null, true); } else { cb(new Error("只支持 PNG/JPG/WebP 格式图片")); } }, }); await callNodeListener( // @ts-expect-error multer 中间件类型 upload.single("file"), event.node.req, event.node.res, ); // @ts-expect-error multer 挂载 file const file = event.node.req.file as MulterUploadedFile | undefined; if (!file?.path) { throw createError({ statusCode: 400, statusMessage: "请选择要上传的图片" }); } try { const result = await replaceMediaAssetFileFromTempUpload({ assetId, actorUserId: admin.id, actorIsAdmin: true, tempInputPath: file.path, }); return R.success({ url: `${POST_MEDIA_PUBLIC_PREFIX}${result.storageKey}`, sizeBytes: result.sizeBytes, }); } catch (err) { if (file.path && fs.existsSync(file.path)) { try { fs.unlinkSync(file.path); } catch { /* ignore */ } } throw err; } });